Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@remote-ui/rpc
Advanced tools
An RPC library with strong support for simulating the transfer of functions via postMessage
@remote-ui/rpc is a package that facilitates remote procedure calls (RPC) between different contexts, such as between a web worker and the main thread. It allows for seamless communication and function invocation across these boundaries, making it easier to build modular and decoupled applications.
Creating an RPC endpoint
This feature allows you to create an RPC endpoint. The `createEndpoint` function is used to set up an endpoint in a given context, such as a web worker.
const {createEndpoint} = require('@remote-ui/rpc');
const endpoint = createEndpoint(self);
Defining callable functions
This feature allows you to define functions that can be called remotely. The `expose` function is used to make these functions available to the other context.
const {expose} = require('@remote-ui/rpc');
function add(a, b) {
return a + b;
}
expose({add});
Calling remote functions
This feature allows you to call functions that are defined in another context. The `createRemote` function is used to create a remote object that can call the exposed functions.
const {createEndpoint, createRemote} = require('@remote-ui/rpc');
const endpoint = createEndpoint(self);
const remote = createRemote(endpoint);
async function performAddition() {
const result = await remote.call.add(1, 2);
console.log(result); // 3
}
performAddition();
Comlink is a library that simplifies the use of WebWorkers by abstracting away the postMessage API and providing a more intuitive interface for RPC. It allows you to expose functions from a worker and call them from the main thread, similar to @remote-ui/rpc. However, Comlink is more focused on WebWorkers specifically, whereas @remote-ui/rpc can be used in a broader range of contexts.
json-rpc-2.0 is a library that implements the JSON-RPC 2.0 protocol, which is a standard protocol for RPC. It provides a way to define and call remote procedures using JSON messages. While it is more general-purpose and can be used in various environments, it requires more boilerplate code compared to @remote-ui/rpc, which is designed to be more straightforward and easy to use.
rpc-websockets is a library that provides RPC over WebSockets. It allows you to define and call remote procedures over a WebSocket connection. This is useful for real-time applications that require persistent connections. Compared to @remote-ui/rpc, rpc-websockets is more suitable for scenarios where you need a continuous connection, while @remote-ui/rpc is more versatile for different contexts.
@remote-ui/rpc
This library provides a powerful remote procedure call (RPC) abstraction for the rest of the remote-ui libraries. The key feature of this RPC layer is the ability for functions to be passed across a postMessage
interface, which makes sending messages between postMessage
-compatible objects more ergonomic, and supports the common need for passing event callbacks as remote component properties.
Using yarn
:
yarn add @remote-ui/rpc
or, using npm
:
npm install @remote-ui/rpc --save
@remote-ui/core
uses JavaScript’s native Map
, Set
, WeakSet
. It also uses numerous language constructs that require the Symbol
global. This package also makes heavy use of Promises and async/ await, which are based on generators.
Polyfills for all of these features (via core-js
and regenerator-runtime) are imported automatically with the “default” version of this package. If you have a build system that is smart about adding polyfills, you can configure it to prefer (and process) a special build meant to minimize polyfills.
Most developers will not need to know about @remote-ui/rpc
. The comprehensive example shows how a developer can use the higher-level abstractions available in @remote-ui/core
and @remote-ui/web-workers
to build a powerful, remote-ui-powered UI. However, users with more complex needs may need to use this library directly, such as those constructing remote environments around an object other than a web worker.
createEndpoint()
The main export this library provides is createEndpoint
, which creates an Endpoint
object. An Endpoint
wraps a postMessage
interface, managing all messages sent and received. The example below shows how an Endpoint
seamlessly wraps a Worker
object in the browser:
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker);
This Endpoint
object expects to have a “sibling” on the other side of the postMessage
interface. In the case of a web worker, we would construct the sibling inside our worker.js
file like so (assuming a build process compiled the following into browser-friendly JavaScript):
import {createEndpoint} from '@remote-ui/rpc';
const endpoint = createEndpoint(self);
Right now, these endpoints have nothing to “talk about”. An Endpoint
needs to expose
methods to its sibling. For example, we can expose a function in our worker file that will return a message to the main thread:
import {createEndpoint} from '@remote-ui/rpc';
const endpoint = createEndpoint(self);
endpoint.expose({sayHello});
function sayHello() {
return 'Hey :)';
}
Finally, our original Endpoint
can call
its sibling’s new method:
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker);
endpoint.call.sayHello().then((result) => console.log(`They said: ${result}`));
This seemingly-magic call
property is a Proxy
that will forward all method calls via message passing to the sibling Endpoint
. If you are in an environment without Proxy
, you must supply the callable
option when constructing your endpoint, and you must list all methods you will ever call on the sibling Endpoint
:
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker, {
callable: ['sayHello'],
});
endpoint.call.sayHello().then((result) => console.log(`They said: ${result}`));
You’ll notice that the sayHello()
method returns a Promise
when invoked in this way, even though the original function in the worker was synchronous. This is a key thing to understand about this library: since it implements these “function calls” via postMessage
, all functions passing between the Endpoints
become asynchronous.
The example above illustrates a very simple function that accepted no arguments. The real power of @remote-ui/rpc
is that it allows you to pass arguments to these functions, including arguments that themselves contain functions the other side of the Endpoint
will need to call:
// in `worker.ts`:
import {createEndpoint} from '@remote-ui/rpc';
const endpoint = createEndpoint(self);
endpoint.expose({sayHello});
interface User {
fullName(): string | Promise<string>;
}
async function sayHello(user: User) {
return `Hey, ${await user.fullName()}!`;
}
// back on the main thread:
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker);
const user = {
fullName() {
return 'Shoppy the bag';
},
};
// Eventually prints `They said: Hey, Shoppy the bag!
endpoint.call
.sayHello(user)
.then((result) => console.log(`They said: ${result}`));
Notice that, like the sayHello
function itself, the fullName
function on the user
object is automatically implemented using message passing, and becomes asynchronous as a result, even though its original implementation was synchronous.
If you are deeply familiar with RPC libraries, you may be concerned about memory for these functions being leaked. @remote-ui/rpc
is smart enough to clean up the memory for proxied functions in an example like the one above automatically, but complex uses can necessitate additional, manual memory management. This is discussed in the documentation for retain()
and release()
.
Endpoint
An Endpoint
object has a collection of methods and properties for progressively mutating the way it will respond to, or communicate with, its sibling.
Endpoint#call
This property exposes all the methods available on the Endpoint
’s sibling. By default, this property will be a proxy that passes all methods across, and throws asynchronously if the method is not defined on the sibling. If your environment does not support proxies, or you want only designated methods to be callable, you can pass the callable
option when constructing the endpoint, or add additional callable methods with Endpoint#callable()
.
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker, {
callable: ['sayHello'],
});
endpoint.call.sayHello().then(console.log);
If you are using TypeScript, you can supply a type parameter to createEndpoint
for an interface with the methods the sibling will expose. call
will then be strongly typed to ensure you pass the correct arguments.
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
interface WorkerApi {
sayHello(name: string): string;
}
const endpoint = createEndpoint<WorkerApi>(worker, {
// will ensure you only provide valid method names
callable: ['sayHello'],
});
// Type error: sayGoodbye is not defined!
endpoint.call.sayGoodbye().then(console.log);
// Type error: sayHello expects a `string` argument!
endpoint.call.sayHello().then(console.log);
Endpoint#expose()
This is effectively the opposite end of a sibling’s call
property. This method accepts an object with the methods that will be run when the sibling runs call.propertyName()
. These methods can only accept the “simple types” the RPC library supports. Additionally, any functions they accept, either directly or nested in objects/ arrays, must be considered to at least sometimes return promises (as they always will when the function came from a different Endpoint
).
import {createEndpoint, SafeRpcArgument} from '@remote-ui/rpc';
const endpoint = createEndpoint(self);
endpoint.expose({sayHello});
interface User {
fullName(): string;
}
// When using TypeScript, we can use the helper `SafeRpcArgument` type,
// which will ensure any methods, even ones deeply nested, include a function
// return type.
async function sayHello(user: SafeRpcArgument<User>) {
return `Hey, ${await user.fullName()}!`;
}
Endpoint#callable()
Endpoint
s can incrementally expose additional methods using expose()
. However, Endpoint
s created with a callable
argument are “locked” to the original set of methods. The callable
method exposes additional callable properties on Endpoint#call
for each string you pass.
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker, {
callable: [],
});
endpoint.callable('sayGoodbye');
endpoint.call.sayGoodbye().then(console.log);
Endpoint#terminate()
The terminate
method on an Endpoint
will clear out all internal storage of the endpoint, and call terminate
on the postMessage
interface, if it exists.
Endpoint#replace()
The replace
method on an Endpoint
will replace the postMessage
interface being used. When called, the Endpoint
will stop listening for messages on the original, and will send all subsequent messages to the new postMessage
instead. You should only use this feature if you really know what you’re doing™.
Implementing functions using message passing always leaks memory. The implementation in this library involves storing a unique identifier for each function sent between sibling Endpoint
s; when this identifier is received by the sibling, it recognizes it as a “function identifier”. It then maps this function to its existing representation for that ID (if it has been sent before), or creates a new function for it. This function, when called, will send a message to the original source of the function, listing the ID of the function to call (alongside the arguments and other metadata). However, because the two environments need to be able to reference the function and its proxy by ID, it can never release either safely.
@remote-ui/rpc
implements some smart defaults that make memory management a little easier. By default, a function is only retained for the lifetime of its “parent” — the function call that caused the function to be passed. Let’s look at an example of an Endpoint
that accepts a function (here, as the user.fullName
method):
// in `worker.ts`:
import {createEndpoint} from '@remote-ui/rpc';
const endpoint = createEndpoint(self);
endpoint.expose({sayHello});
interface User {
fullName(): string | Promise<string>;
}
async function sayHello(user: User) {
return `Hey, ${await user.fullName()}!`;
}
The sibling would call this method like so:
// back on the main thread:
import {createEndpoint} from '@remote-ui/rpc';
const worker = new Worker('worker.js');
const endpoint = createEndpoint(worker);
const user = {
fullName() {
return 'Shoppy the bag';
},
};
endpoint.call.sayHello(user).then(console.log);
A naive implementation would retain the user.fullName
function forever, even after the sayHello()
call was long gone, and even if user
would otherwise have been garbage collected. However, with @remote-ui/rpc
, this function is automatically released after sayHello
is done. It does so by marking the function as used (“retained”) when sayHello
starts, then marking it as unused when sayHello
is finished. When a function is marked as completely unused, it automatically cleans up after itself by removing the memory in the receiving Endpoint
, and sending a message to its source Endpoint
to release that memory, too.
async function sayHello(user: User) {
// user.fullName is retained automatically here
return `Hey, ${await user.fullName()}!`;
// just before we finish up and send the message with the result,
// we release user, which also releases user.fullName
}
This automatic behavior is problematic if you want to hold on to a function received via @remote-ui/rpc
and call it later, after the function that received it has finished. To address this need, this library provides two functions for manual memory management: retain
and release
.
retain()
As noted above, you will retain()
a value when you want to prevent its automatic release. Calling retain
will, by default, deeply retain the value — that is, it will traverse into nested array elements and object properties, and retain every retain
-able thing it finds. You will typically use this alongside also storing that value in a variable that lives outside the context of the function.
import {retain} from '@remote-ui/rpc';
const allUsers = new Set<User>();
async function sayHello(user: User) {
allUsers.add(user);
retain(user);
return `Hey, ${await user.fullName()}!`;
}
Once you have explicitly retain
ed a value, it will never be released until the Endpoint
is terminated, or a matching number of release()
calls are performed on the object.
release()
Once you are no longer using the a retain
-ed value, you must release
it. Like retain()
, this function will apply to all nested array elements and object properties.
import {release} from '@remote-ui/rpc';
const allUsers = new Set<User>();
function removeUser(user: User) {
allUsers.delete(user);
release(user);
}
Once an object is fully released, any attempt to call its proxied functions will result in an error.
Not all types of arguments are supported for functions proxied over postMessage
by @remote-ui/rpc
. Only the following simple types can be used:
true
, false
, null
, and undefined
This excludes many types, but of particular note are the following restrictions:
Map
, Set
, WeakMap
, or WeakSet
ArrayBuffer
or typed arraysURL
or RegExp
instanceof
or similar checks on the transferred value)This library also provides a collection of adaptors that transform common postMessage
-related objects into an Endpoint
. You can of course write your own adaptor for these (and may need to, if you have complex needs), but these adaptors can be a helpful starting point:
fromWebWorker()
allows you to create an Endpoint
from a Web Worker:
import {createEndpoint, fromWebWorker} from '@remote-ui/rpc';
const worker = new Worker('./worker.js', import.meta.url);
const endpoint = createEndpoint(fromWebWorker(worker));
fromMessagePort()
allows you to create an Endpoint
from a MessagePort
object
import {createEndpoint, fromMessagePort} from '@remote-ui/rpc';
const channel = new MessageChannel();
const endpoint = createEndpoint(fromMessagePort(channel.port2));
fromIframe()
allows you to create an Endpoint
from a browser window by connecting it to a child iframe
:
import {createEndpoint, fromIframe} from '@remote-ui/rpc';
const iframe = document.createElement('iframe');
iframe.setAttribute('src', '/my-iframe-page');
document.append(iframe);
const endpoint = createEndpoint(fromIframe(iframe));
// Optionally, you can pass {terminate: false} to prevent the iframe from being
// removed from the DOM when the endpoint is terminated:
const endpoint2 = createEndpoint(fromIframe(iframe, {terminate: false}));
fromInsideIframe()
allows you to create an Endpoint
from a parent browser window, from within a child iframe
:
// Can only be run from inside an iframe, where `self.parent` is available
import {createEndpoint, fromInsideIframe} from '@remote-ui/rpc';
const endpoint = createEndpoint(fromInsideIframe());
FAQs
An RPC library with strong support for simulating the transfer of functions via postMessage
The npm package @remote-ui/rpc receives a total of 132,825 weekly downloads. As such, @remote-ui/rpc popularity was classified as popular.
We found that @remote-ui/rpc demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.